
import pygame

from OpenGL.GL import *
from OpenGL.GLU import *
#import pyoglmisc

from Box2D2 import *

import os
import glob
import xml.dom.minidom as xmlHandler
import time
import copy
import math
import struct
#import numpy

import random

# TODO: Decide whether Camera and Texture(*) should be moved into their own modules,
#        or whether this module should just be renamed Graphics

# Center the pygame window on creation
os.environ['SDL_VIDEO_CENTERED'] = '1'

class Renderer(object):
    """
    Dependencies: pygame, OpenGL.GL, OpenGL.GLU
    """
    def __init__(self):
        self.camera = Camera()
        self.camera.zoomDirectTo(2.0)
        
        self.width = 0
        self.height = 0
        self.aspect = 1.0
        self.motionBlurIsOn = True
        self.motionBlurFactor = 0.0
        
        self.layers = []
        self.default_layer = None

        self.is_parallax_enabled = (True, False)
    
    def init(self, width, height, fullscreen=False, forceResolution=False):
        # Set display resolution
        self.width, self.height = width, height
        if not forceResolution:
            self.width, self.height = self.findClosestAvailableResolutionTo(self.width, self.height)
        self.aspect = self.height / float(self.width)
        flags = pygame.OPENGL | pygame.DOUBLEBUF
        if fullscreen:
            flags |= pygame.FULLSCREEN
        pygame.display.set_mode((self.width, self.height), flags)
        
        self.initOpenGL()
        
        self.textureLoader = TextureLoader()
        self.textureLoader.loadResources()

        self.meshLoader = MeshLoader()
        self.meshLoader.loadResources()
        # ...
    
    def getSizeInPxls(self):
        return (self.width, self.height)
    def getSize(self):
        return (2.0, 2.0*self.aspect)
    def getMinCoords(self):
        return (-1.0, -self.aspect)
    def getMaxCoords(self):
        return (1.0, self.aspect)
    def enableMotionBlur(self):
        glClear(GL_ACCUM_BUFFER_BIT)
        self.motionBlurIsOn = True
    def disableMotionBlur(self):
        self.motionBlurIsOn = False
    def toggleMotionBlur(self):
        glClear(GL_ACCUM_BUFFER_BIT)
        self.motionBlurIsOn = not self.motionBlurIsOn
    def enableParallax(self, horiz, vert):
        self.is_parallax_enabled = (horiz, vert)
    
    def findClosestAvailableResolutionTo(self, width, height):
        modeList = pygame.display.list_modes()
        nextMode = []
        for mode in modeList:
            if mode[0] >= width and mode[1] >= height:
                nextMode.append(mode)
        bestX, bestY = -1,-1
        for mode in nextMode:
            if bestX == -1 or bestX >= mode[0]:
                if bestY == -1 or bestY >= mode[1]:
                    bestX, bestY = mode[0], mode[1]
        return bestX, bestY

    class Layer(object):
        def __init__(self, name, parallax):
            self.name = name
            self.parallax = parallax
            self.models_to_render = []

    def addLayer(self, name, parallax):
        self.layers.append(Renderer.Layer(name, parallax))

    def setDefaultLayer(self, layer):
        self.default_layer = layer

    def getLayer(self, name):
        found_layer = None
        for layer in self.layers:
            if layer.name == name:
                found_layer = layer
                break
        if found_layer:
            return found_layer
        else:
            raise Exception("Layer '" + name + "' not known.")

    def getDefaultLayer():
        if self.default_layer:
            return self.default_layer
        else:
            raise Exception("Attempt to access default layer when one has not been set.")

    """
    def addModelToLayer(self, model, layer=None):
        # Allow layer to be given as the default, as a string name, or as an
        # instance.
        if layer == None: layer = self.getDefaultLayer()
        elif layer.__class__ != Model: layer = self.getLayer(layer)
        layer.models_to_render.append(model)
    """
    def createModel(self, layer=None, *args, **kwargs):
        """Instantiates a model and adds it to the given layer for rendering.
        """
        # Allow layer to be given as the default, as a string name, or as an
        # instance.
        if layer == None: layer = self.getDefaultLayer()
        elif layer.__class__ != Model: layer = self.getLayer(layer)
        
        model = Model(self, layer, *args, **kwargs)
        # TODO: Rename models_to_render to simply models
        layer.models_to_render.append(model)
        return model

    def destroyModel(self, model):
        model.layer.models_to_render.remove(model)
    
    def initOpenGL(self):
        glClearColor(0.0, 0.0, 0.0, 1.0)
        glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT)
        
        glClearAccum(0.0, 0.0, 0.0, 1.0)
        glClear(GL_ACCUM_BUFFER_BIT)
        
        glMatrixMode(GL_PROJECTION)
        glLoadIdentity();
        #TODO: Rewrite this comment when fully understand
        # this puts us in quadrant 1, rather than quadrant 4
        #gluOrtho2D(0, self.width, self.height, 0)
        gluOrtho2D(-1, 1, -self.aspect, self.aspect)
        glMatrixMode(GL_MODELVIEW)
        
        glDisable(GL_DEPTH_TEST)
        glEnable(GL_TEXTURE_2D)
        glEnable(GL_BLEND)
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
        #Enable back-face culling
        glEnable(GL_CULL_FACE)
        glCullFace(GL_BACK)
        
    def render(self):

        # Apply camera rotation and zoom
        glRotatef(self.camera.getAngle(), 0, 0, 1)
        glScalef(self.camera.getZoom(), self.camera.getZoom(), 1.0)
        
        for layer in self.layers:
            # Apply camera translation with per-layer parallax scrolling
            glPushMatrix()
            if self.is_parallax_enabled[0]:
                horiz_parallax = layer.parallax
            else:
                horiz_parallax = 1
            if self.is_parallax_enabled[1]:
                vert_parallax = layer.parallax
            else:
                vert_parallax = 1
            glTranslatef(horiz_parallax * -self.camera.getPos()[0], \
                         vert_parallax * -self.camera.getPos()[1], 0.0)
            # Render the models in the layer
            for model in layer.models_to_render:
                model._render()

            # Render axis\scale markers
            glBindTexture(GL_TEXTURE_2D, 0)
            glBegin(GL_LINES)
            glColor4f(1, 0, 0, 0.5); glVertex3f(0, 0, 0); glVertex3f(0.1, 0, 0)
            glColor4f(0, 1, 0, 0.5); glVertex3f(0, 0, 0); glVertex3f(0, 0.1, 0)
            glEnd()
            
            glPopMatrix()
            #layer.models_to_render = []
        
        if self.motionBlurIsOn:
            glAccum(GL_MULT, self.motionBlurFactor)
            glAccum(GL_ACCUM, 1-self.motionBlurFactor)
            glAccum(GL_RETURN, 1.0)
            glFlush()
        
        pygame.display.flip()
        
        glClear(GL_COLOR_BUFFER_BIT)#|GL_DEPTH_BUFFER_BIT)
        
        #glClear(GL_ACCUM_BUFFER_BIT)
        
        #glAccum(GL_ACCUM, 0.5)
        #glAccum(GL_RETURN, 1)
        
        glLoadIdentity()
        
class Camera(object):
    """
    TODO: Implement auto movement between current and desired camera values
    Dependencies: PyOpenGL, (Vector)
    """
    class Zoom(object):
        def __init__(self):
            self.value = 1.0
            #self.pinned = True
            self.desiredValue = self.value
            self.accel = 0.01
            self.changeRate = 0.0
            self.maxChangeRate = 0.01
            self.damping = 0.8
    class Pos(object):
        def __init__(self):
            self.value = (0, 0, 0)
            #self.pinned = True
            self.desiredValue = self.value
            self.accel = 0.0
            self.changeRate = 0.0
            self.maxChangeRate = 0.01
            self.damping = 0.8
    class Angle(object):
        def __init__(self):
            self.value = 0.0
            #self.pinned = True
            self.desiredValue = self.value
            self.accel = 0.0
            self.changeRate = 0.0
            self.maxChangeRate = 0.01
            self.damping = 0.8
    def __init__(self):
        self.zoom = self.Zoom()
        self.pos = self.Pos()
        self.angle = self.Angle()
        
        self.xform = b2XForm()
        
        self.isFollowing = False
        self.bodyToFollow = None
        
    def update(self, timeStep):
        """
        Computes the new the zoom, position and angle values, and applies them
        as OpenGL transformations.
        """
        self.zoom.changeRate += timeStep * self.zoom.accel * (self.zoom.desiredValue-self.zoom.value)
        self.zoom.changeRate *= self.zoom.damping
        if self.zoom.changeRate > self.zoom.maxChangeRate:
            self.zoom.changeRate = self.zoom.maxChangeRate
        self.zoom.value += timeStep * self.zoom.changeRate
        
        if self.isFollowing:
            self.moveDirectTo(self.bodyToFollow.GetXForm().position.tuple())
        else:
            # TODO: Repeat motion smoothing for pos, but need to decide on Vector class usage first
            pass
        
        self.angle.changeRate += timeStep * self.angle.accel * (self.angle.desiredValue-self.angle.value)
        self.angle.changeRate *= self.angle.damping
        if self.angle.changeRate > self.angle.maxChangeRate:
            self.angle.changeRate = self.angle.maxChangeRate
        self.angle.value += timeStep * self.angle.changeRate
    
    def applyXForm(self):
        # Create a transform object representing the camera
        self.xform = b2XForm( b2Vec2(-self.pos.value[0], -self.pos.value[1]), \
                              b2Mat22(math.radians(self.angle.value)) )
        # Add the scaling by the zoom
        #scaleMatrix = b2Mat22(self.zoom.value, 0, \
        #                      0,               self.zoom.value)
        #self.xform.R = box2D.b2MulT(scaleMatrix, self.xform.R)
        
        glScalef(self.zoom.value, self.zoom.value, 1)
        #glRotatef(self.angle.value, 0, 0, 1)
        #glTranslatef(-self.pos.value[0], -self.pos.value[1], 0)
        # Apply the transformation in one OpenGL matrix multiply
        glMultMatrixf(self.xform.GetOpenGLForm())
    
    def getZoom(self):
        return self.zoom.value
    def getPos(self):
        return self.pos.value
    def getAngle(self):
        return self.angle.value
        
    def zoom(self, value):
        self.zoom.value += value
    def zoomTo(self, zoom):
        self.zoom.desiredValue = zoom
    def zoomDirectTo(self, zoom):
        self.zoom.desiredValue = zoom
        self.zoom.value = zoom
    def move(self, value):
        self.pos.value += value
    def moveTo(self, pos):
        self.pos.desiredValue = pos
    def moveDirectTo(self, pos):
        self.pos.desiredValue = pos
        self.pos.value = pos
    def rotate(self, value):
        self.angle.value += value
    def rotateTo(self, angle):
        self.angle.desiredValue = angle
    def rotateDirectTo(self, angle):
        self.angle.desiredValue = angle
        self.angle.value = angle
    
    def setZoomAccel(self, accel):
        self.zoom.accel = accel
    def setMoveAccel(self, accel):
        self.pos.accel = accel
    def setRotateAccel(self, accel):
        self.angle.accel = accel
    def setMaxZoomRate(self, rate):
        self.zoom.maxChangeRate = rate
    def setMaxMoveRate(self, rate):
        self.pos.maxChangeRate = rate
    def setMaxRotateRate(self, rate):
        self.angle.maxChangeRate = rate
    def setZoomDamping(self, value):
        self.zoom.damping = value
    def setMovementDamping(self, value):
        self.pos.damping = value
    def setRotationDamping(self, value):
        self.angle.damping = value
        
    def followBody(self, body):
        """Forces the camera to always centre on body.
           
           Will work for any object that implements GetXForm()
        """
        self.isFollowing = True
        self.bodyToFollow = body
    def stopFollowing(self):
        self.isFollowing = False

# TODO: Add optional event callback on animation completion
class Texture(object):
    def __init__(self):
        self.sourceFilename = "<unloaded source texture>"
        self.name = "<unloaded texture>"
        self.sourceWidth = 0
        self.sourceHeight = 0
        self.rect = (0, 0, 0, 0)
        self.bitDepth = 0
        self.index = -1
        self.frameCount = 1
        self.frameRows = 1
        
        self.frame = 0
        self.isAnimating = False
        self.isOnLoop = True
        self.frameRate = 25
        self.lastFrameTime = 0
    
    def bind(self):
        glMatrixMode(GL_TEXTURE)
        # Reset the texture matrix
        glLoadIdentity()
        # Transform to this texture's region of its source texture
        glTranslatef(self.rect[0]/float(self.sourceWidth), 1.0-(self.rect[1]+self.rect[3])/float(self.sourceHeight), 0)
        glScalef(self.rect[2] / float(self.sourceWidth), self.rect[3] / float(self.sourceHeight), 1.0)
        frameCols = (self.frameCount+1) / self.frameRows
        # Offset to the current animation frame
        glTranslatef(self.frame % frameCols, -(self.frame / frameCols), 0)
        glMatrixMode(GL_MODELVIEW)
        glBindTexture(GL_TEXTURE_2D, self.index)
        
    def unbind(self):
        # Convenience\wrapper function
        glBindTexture(GL_TEXTURE_2D, 0)
    
    def startAnimating(self, engine):
        engine.addEventListener("tick", self.update)
        self.isAnimating = True
        self.frame = 0
        self.lastFrameTime = time.time()
    
    def update(self, engine, timeStep):
        if self.isAnimating:
            if time.time() - self.lastFrameTime >= 1.0 / self.frameRate:
                if self.frame < self.frameCount - 1:
                    self.frame += 1
                    self.lastFrameTime = time.time()
                else:
                    if self.isOnLoop:
                        self.frame = 0
                        #self.lastFrameTime = time.time()
                        self.lastFrameTime += 1.0 / self.frameRate
                    else:
                        self.isAnimating = False

class TextureError(Error):
    pass
class TextureLoader(object):
    """Self explanatory
    
    Dependencies: xml.dom.minidom, os, glob, PyOpenGL, PyGame
    """
    
    def __init__(self):
        self.textures = {}
        self.path = os.path.join(os.getcwd(), 'Textures')
    def __del__(self):
        for texture in self.textures:
            glDeleteTextures(1, texture.index)
    
    def loadResources(self):
        """Call on engine init to load texture info files
        """
        for file in glob.glob( os.path.join(self.path, '*.xml') ):
            xmlDoc = xmlHandler.parse(file)
            xmlOuden = xmlDoc.getElementsByTagName("ouden")[0]
            for xmlSourceTexInfo in xmlOuden.getElementsByTagName("sourcetexture"):
                self.__loadSourceTextureInfo(xmlSourceTexInfo)
        
        # Also add a special case None texture
        texture = Texture()
        texture.name = "None"
        texture.sourceName = "None"
        texture.index = 0
        texture.sourceWidth = 1
        texture.sourceHeight = 1
        texture.rect = (0, 0, 1, 1)
        self.textures[texture.name] = texture
        
    def __loadSourceTextureInfo(self, xmlSourceTexInfo):
        for xmlTexInfo in xmlSourceTexInfo.getElementsByTagName("texture"):
            texture = Texture()
            texture.sourceFilename = xmlSourceTexInfo.getAttribute("filename")
            self.__loadTextureInfo(xmlTexInfo, texture)
    
    def __loadTextureInfo(self, xmlTexInfo, texture):
        texture.name = xmlTexInfo.getAttribute("name")
        rectString = xmlTexInfo.getAttribute("rect")
        texture.rect = tuple( [int(x) for x in (rectString.split(","))] )
        frameCount = xmlTexInfo.getAttribute("frameCount")
        if frameCount != '':
            texture.frameCount = int(frameCount)
        frameRows = xmlTexInfo.getAttribute("frameRows")
        if frameRows != '':
            texture.frameRows = int(frameRows)
        frameRate = xmlTexInfo.getAttribute("frameRate")
        if frameRate != '':
            texture.frameRate = int(frameRate)
        # Add the new texture to the dictionary
        self.textures[texture.name] = texture
        
        #print "Loaded texture"
        #print "name: ", texture.name
        #print "rect: ", texture.rect
    
    def getTexture(self, name):
        if name is None:
            return copy.copy(self.textures["None"])
        # Check if texture is known (i.e., has a corresponding xml info file)
        elif self.textures.has_key(name):
            # Check if texture's source image file has been loaded
            if self.textures[name].index is -1:
                self.__loadSourceTexture(self.textures[name].sourceFilename, self.textures[name])
            return copy.copy(self.textures[name])
        # Check for unknown texture file (i.e., one w\o a corresponding xml info file)
        # i.e., fall back to basic texture loading functionality
        # TODO: Make this act like the modern texture loading system where subsequent calls do not re-load the texture!
        elif os.path.isfile(os.path.join(self.path, name)):
            texture = Texture()
            # Create a basic texture whose rect is simply the whole source texture
            texture.name = name
            self.__loadSourceTexture(name, texture)
            texture.rect = (0, 0, texture.sourceWidth, texture.sourceHeight)
            # Add the new texture to the dictionary
            self.textures[name] = texture
            return texture
        else:
            raise TextureError("Texture \"" + name + "\" is not known and no texture file with this name was found in " + self.path) 
        
    def __loadSourceTexture(self, filename, texture):
        surface = pygame.image.load( os.path.join(self.path, filename) )
        
        if surface.get_bitsize() == 24:
            format = 'RGB'
            #surface.set_colorkey(pygame.Color("green"))
            #surface.convert(pygame.Surface)
            #rawData = [(r, g, b, g==1) for 
        elif surface.get_bitsize() == 32:
            format = 'RGBA'
        else:
            raise TextureError("Source texture \"" + filename + "\"\'s bitDepth (" + surface.get_bitsize() + ") is too small. Needs to be 24 or 32 bpp.")
        rawData = pygame.image.tostring(surface, format, True)
        texture.sourceWidth = surface.get_width()
        texture.sourceHeight = surface.get_height()
        texture.bitDepth = surface.get_bitsize()
        texture.index = glGenTextures(1)
        glBindTexture(GL_TEXTURE_2D, texture.index)
        if texture.bitDepth is 24: format = GL_RGB
        elif texture.bitDepth is 32: format = GL_RGBA
        
        gluBuild2DMipmaps(GL_TEXTURE_2D, \
                          format, \
                          texture.sourceWidth, \
                          texture.sourceHeight, \
                          format, \
                          GL_UNSIGNED_BYTE, \
                          rawData)
        #Set texture properties 
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR)
        # TODO: Check OpenGL version supports GL_CLAMP_TO_EDGE
        # TODO: Check if we actually need this here
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)
        # To avoid confusion and later bugs
        glBindTexture(GL_TEXTURE_2D, 0)

"""
<ouden>
    <material name="bump_mapped" num_lights_used=1>
        <vertex_shader filename="my_shader.vert"/>
        <fragment_shader>
            GLSL code
        </fragment_shader>
        <attribute name="diffuse_texture", type="texture", default="rocky" />
        <attribute name="normal_map", type="texture", default = "rocky_normal" />
        <attribute name="ambient_color", type="color", default="1, 1, 1, 1" />
        <attribute name="shininess", type="float", default="0.5" />
    </material>
</ouden>
"""
"""
# Initialisation
# TODO: Create program
# TODO: Compile shaders
glUseProgramObject(my_program)
glUniform1i( glGetUniformLocation(program, "diffuse_map"), 0)
glUniform1i( glGetUniformLocation(program, "normal_map"), 1)
"""

class Light:
    def __init__(self):
        self.ambient = (0, 0, 0, 1)
        self.diffuse = (1, 1, 1, 1)
        self.specular = (1, 1, 1, 1)
        self.pos = (0, 0, 1, 0)
        self.spot_direction = (0, 0, -1)
        self.spot_exponent = 0
        self.spot_cutoff = 180
        self.const_attenuation = 1
        self.linear_attenuation = 0
        self.quad_attenuation = 0

    def bind(self, light_index):
        light_index = GL_LIGHT0 + light_index
        glLight(light_index, GL_AMBIENT, self.ambient)
        glLight(light_index, GL_DIFFUSE, self.diffuse)
        glLight(light_index, GL_SPECULAR, self.specular)
        glLight(light_index, GL_POSITION, self.pos)
        glLight(light_index, GL_SPOT_DIRECTION, self.spot_direction)
        glLight(light_index, GL_SPOT_EXPONENT, self.spot_exponent)
        glLight(light_index, GL_SPOT_CUTOFF, self.spot_cutoff)
        glLight(light_index, GL_CONSTANT_ATTENUATION, self.const_attenuation)
        glLight(light_index, GL_LINEAR_ATTENUATION, self.linear_attenuation)
        glLight(light_index, GL_QUADRATIC_ATTENUATION, self.quad_attenuation)

class Material:
    def __init__(self, renderer, vert_shader_name, frag_shader_name, light_count=3):
        self.shader_program = None
        self.light_count = 0
        self.textures = []

        # TODO: Move this into material loader
        self.path = os.path.join(os.getcwd(), "Textures")

        #self.vert_shader = glCreateShader(GL_VERTEX_SHADER)
        # TODO: Load shader source from file into vert_shader_source
        vert_shader_file = open(os.path.join(self.path, "bump_mapped.vert"), \
                                'r')
        vert_shader_source = vert_shader_file.read()
        #glShaderSource(self.vert_shader, vert_shader_source)
        #glCompileShader(self.vert_shader)
        
        #self.frag_shader = glCreateShader(GL_FRAGMENT_SHADER)
        # TODO: Load shader source from file into frag_shader_source
        frag_shader_file = open(os.path.join(self.path, "bump_mapped.frag"), \
                                'r')
        frag_shader_source = frag_shader_file.read()
        #glShaderSource(self.frag_shader, frag_shader_source)
        #glCompileShader(self.frag_shader)

        #self.shader_program = glCreateProgram()
        #glAttachShader(self.shader_program, self.vert_shader)
        #glAttachShader(self.shader_program, self.frag_shader)
        #glLinkProgram(self.shader_program)

        self.shader_program = pyoglmisc.compile_program(vert_shader_source, \
                                                        frag_shader_source)

        glUseProgram(self.shader_program)
        self.init_shaders()

        texture_loader = renderer.textureLoader
        self.textures.append(texture_loader.getTexture('rockbump_diffuse.jpg'))
        self.textures.append(texture_loader.getTexture('rockbump_normal.png'))

    def init_shaders(self):
        glUseProgram(self.shader_program)
        glUniform1i( glGetUniformLocation(self.shader_program, "diffuse_map"), 0)
        glUniform1i( glGetUniformLocation(self.shader_program, "normal_map"), 1)
        glUniform1f( glGetUniformLocation(self.shader_program, "invRadius"), 1.0)

    def bind(self):
        for i in range(len(self.textures)):
            glActiveTexture(GL_TEXTURE0 + i)
            self.textures[i].bind()
        glUseProgram(self.shader_program)
        # TODO: Decide where the lighting will be applied
        # ...
        
    def unbind(self):
        glUseProgram(0)
        glBindTexture(0)

class SimpleMaterial(Material):
    def __init__(self, texture, color):
        Material.__init__(self, vert_shader="simple", frag_shader="simple", \
                          light_count=3)

        self.textures.append(texture)

    def init_shaders(self):
        Material.init_shaders(self)
        glUniform1i( glGetUniformLocation(self.shader_program, "texture"), 0)

"""
def renderModel(model):
	light = findClosestLightTo(model)
	light.setAsOpenGLLight(0)
	
	glUseProgramObject(my_program)
	
	glActiveTexture(GL_TEXTURE0 + 0)
	model.material.texture.bind()
	glActiveTexture(GL_TEXTURE0 + 1)
	model.material.normal_map.bind()
	
	model.material.shaders

class Material:
    def __init__(self):
        self.shader_program = None

        simple_shader:
            texture
            color
            light_count = 0

        bump_mapped_shader:
            texture
            normal_map
            ambient_color
            diffuse_color
            shininess
            close_attenuation
            medium_attenuation
            far_attenuation
            light_count = 3

        skin_shader:
            diffuse_texture
            normal_map
            spec_map
            ambient_color
            diffuse_color
            spec_color
            shininess_bias
            rim_color
            rim_power
            sub_surface_scattering_bias
            light_count = 3

        car_paint_shader:
            diffuse_texture
            normal_map
            ambient_color
            diffuse_color
            shininess
            light_count = 1
            edge_color
            middle_color
            fresnel_bias
            fresnel_power
"""

# New System todos
#   Set of vertices with (N) weighted bindings to bone xforms
#   Vertex data would need to be re-calculated using bone xforms as input

# TODO: Decide if might need this
def isOpenGLExtensionSupported(TargetExtension):
    """ Accesses the rendering context to see if it supports an extension.
            Note, that this test only tells you if the OpenGL library supports
            the extension. The PyOpenGL system might not actually support the extension.
    """
    Extensions = glGetString (GL_EXTENSIONS)
    # python 2.3
    # if (not TargetExtension in Extensions):
    #	gl_supports_extension = False
    #	print "OpenGL does not support '%s'" % (TargetExtension)
    #	return False

    # python 2.2
    Extensions = Extensions.split ()
    found_extension = False
    for extension in Extensions:
            if extension == TargetExtension:
                    found_extension = True
                    break;
    if (found_extension == False):
            gl_supports_extension = False
            print "OpenGL rendering context does not support '%s'" % (TargetExtension)
            return False

    gl_supports_extension = True

    # Now determine if Python supports the extension
    # Exentsion names are in the form GL_<group>_<extension_name>
    # e.g.  GL_EXT_fog_coord 
    # Python divides extension into modules
    # g_fVBOSupported = IsExtensionSupported ("GL_ARB_vertex_buffer_object")
    # from OpenGL.GL.EXT.fog_coord import *
    if (TargetExtension [:3] != "GL_"):
            # Doesn't appear to following extension naming convention.
            # Don't have a means to find a module for this exentsion type.
            return False

    # extension name after GL_
    afterGL = TargetExtension [3:]
    try:
            group_name_end = afterGL.index ("_")
    except:
            # Doesn't appear to following extension naming convention.
            # Don't have a means to find a module for this exentsion type.
            return False

    group_name = afterGL [:group_name_end]
    extension_name = afterGL [len (group_name) + 1:]
    extension_module_name = "OpenGL.GL.%s" % (extension_name)

    try:
            __import__ (extension_module_name)
            print "PyOpenGL supports '%s'" % (TargetExtension)
    except:
            print "OpenGL rendering context supports '%s'" % (TargetExtension),
            print "however PyOpenGL (ver %s) does not." % (OpenGL.__version__)
            return False

    return True

# TODO: Reconsider the structure of these loader classes. For ideal extensibility, need to be able to create new classes for loading in new ways.
# TODO: See if can store a mesh's data once and allow it to be used by multiple models.
# TODO: Think about implementing mesh geometries at different LODs 
class Mesh(object):
    class Vertex(object):
        def __init__(self, pos, tex_coords=b2Vec2(0.0, 0.0), color=(255, 255, 255, 255)):
            self.pos = pos
            self.tex_coords = tex_coords
            self.color = color

        def getAsString(self, format):
            if format == GL_T2F_C4UB_V3F:
                return struct.pack('=2f4B3f', \
                                   self.tex_coords.x, self.tex_coords.y, \
                                   self.color[0], self.color[1], \
                                   self.color[2], self.color[3], \
                                   self.pos.x, self.pos.y, 0.0)
            elif format == GL_T2F_V3F:
                return struct.pack('=2f3f', \
                                   self.tex_coords.x, self.tex_coords.y, \
                                   self.pos.x, self.pos.y, 0.0)
            elif format == GL_V3F:
                return struct.pack('=3f', self.pos.x, self.pos.y, 0.0)
            else:
                raise Exception("Vertex string format '" + str(format) + "' not known.")
            
    def __init__(self, vertices=[], geometry={}, vertex_data=None, geometry_data=None, use_VBOs=False, use_per_vertex_coloring=False, is_dynamic=False):
        """
            geometry: a dict of primitive modes mapped to index lists
            e.g., geometry={GL_POINTS:[],GL_LINE_STRIP:[], \
                            GL_LINE_LOOP:[],GL_LINES:[], \
                            GL_TRIANGLE_STRIP:[], \
                            GL_TRIANGLE_FAN:[],GL_TRIANGLES:[], \
                            GL_QUAD_STRIP:[],GL_QUADS:[], \
                            GL_POLYGON:[]}
        """
        
        if use_per_vertex_coloring:
            self.vertex_data_format = GL_T2F_C4UB_V3F
        else:
            self.vertex_data_format = GL_T2F_V3F
        
        self.is_dynamic = is_dynamic
        
        if vertex_data == None:
            # Transform the given vertex list into a binary data string of a particular format
            self.vertex_data = ''.join([vertex.getAsString(self.vertex_data_format) for vertex in vertices])
            #print "vertex_data:", repr(self.vertex_data)
        else:
            self.vertex_data = vertex_data

        if geometry_data == None:
            # Do a similar thing for the geometry\index data
            self.geometry_data = {}
            for primitive_mode, indices in geometry.items():
                # Set the geometry data as a 2-tuple of the binary data and the number of indices
                self.geometry_data[primitive_mode] = ( ''.join([struct.pack('=H', i) for i in indices]), len(indices) )
                #print "Mode:", primitive_mode, ", geometry_data:", repr(self.geometry_data[primitive_mode])
        else:
            self.geometry_data = geometry_data

        # TODO: Decide whether to hold on to this. Will it be needed for dynamic mesh changes?
        #self.vertices = vertices
        #self.geometry = geometry

        # TODO: Deal with vertex and geometry data being set or changed later
        
        # TODO: Determine whether VBOs are actually available
        self.use_VBOs = use_VBOs
        
        if self.use_VBOs:
            # Create our VBO
            # Note: Older versions of PyOpenGL use this (C-like) syntax instead
            #self.vertex_buffer_object = GLuint(0)
            #glGenBuffers(1, self.vertex_buffer_object)
            self.vertex_buffer_object = int(glGenBuffers(1))
            # Bind our VBO
            glBindBuffer(GL_ARRAY_BUFFER, self.vertex_buffer_object)
            # Copy the data into the bound VBO
            # TODO: The last parameter here, 'usage', should be dynamic iif the vertex data is dynamic, e.g., as in skinning
            if self.is_dynamic: usage_flag = GL_DYNAMIC_DRAW # Or GL_STREAMING_DRAW, if the data will change every frame (e.g., skinned meshes)!
            else: usage_flag = GL_STATIC_DRAW
            glBufferData(GL_ARRAY_BUFFER, self.vertex_data, usage_flag)
            
            # And do a similar thing for the IBOs for each primitive mode
            self.index_buffer_objects = {}
            for primitive_mode, (index_data, index_count) in self.geometry_data.items():
                ibo = int(glGenBuffers(1))
                self.index_buffer_objects[primitive_mode] = (ibo, index_count)
                glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo)
                # TODO: The last parameter here, 'usage', should be dynamic iif the geometry data is dynamic, e.g., different LODs will be used for this mesh
                glBufferData(GL_ELEMENT_ARRAY_BUFFER, index_data, GL_DYNAMIC_DRAW)

            # We no longer need to hold the data in system memory as it's now on the GPU
            self.vertex_data = None
            self.geometry_data = None
            
    def render(self):
        if self.use_VBOs:
            # Bind the VBO in gfx memory
            glBindBuffer(GL_ARRAY_BUFFER, self.vertex_buffer_object)
            # Point OpenGL to the interleaved vertex data stored in the VBO in gfx memory
            glInterleavedArrays(self.vertex_data_format, 0, None)

            for primitive_mode, (ibo, index_count) in self.index_buffer_objects.items():
                # Bind the index buffer object in gfx memory
                glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo)
                glDrawElements(primitive_mode, index_count, GL_UNSIGNED_SHORT, None)
        else:
            # Point OpenGL to the interleaved vertex data
            glInterleavedArrays(self.vertex_data_format, 0, self.vertex_data)

            #for primitive_mode, indices in self.geometry.items():
            for primitive_mode, (index_data, index_count) in self.geometry_data.items():
                #glDrawElementsus(primitive_mode, indices)
                glDrawElements(primitive_mode, index_count, GL_UNSIGNED_SHORT, index_data)

        # Disable the OpenGL client states that were enabled by the glInterleavedArrays function
        glDisableClientState(GL_TEXTURE_COORD_ARRAY)
        glDisableClientState(GL_COLOR_ARRAY)
        glDisableClientState(GL_VERTEX_ARRAY)
        glDisableClientState(GL_INDEX_ARRAY)

# TODO: Finish these skeleton things
class Skeleton(object):
    def __init__(self):
        self.bones = []

class SkinnedMesh(Mesh):
    class BoneInfluence:
        def __init__(self):
            self.bone = None
            self.weight = 0.0
    class SkinnedVertex(object):
        def __init__(self, bone_influences, pos, tex_coords=b2Vec2(0.0, 0.0), color=(255, 255, 255, 255)):
            Mesh.Vertex.__init__(self, pos, tex_coords, color)
            self.bone_influences = bone_influences
            
    def __init__(self, skeleton, vertices=[], geometry={}, use_per_vertex_coloring=True, is_dynamic=False):
        Mesh.__init__(self, vertices, geometry, use_per_vertex_coloring, is_dynamic)
        self.skeleton = skeleton

    def render(self):

        # Apply (SOFTWARE) skinning
        # TODO: Work out how to make the use of VBOs transparent. The below code assumes they are not used!
        for vertex in self.vertices:
            xform = b2XForm()
            for bone_influence in vertex.bone_influences:
                xform += self.skeleton.bones[bone_influence.bone] * bone_influence.weight
            new_pos = b2Mul(xform, vertex.pos)
        
        Mesh.render(self)
        

class MeshLoader(object):
    def __init__(self):
        self.meshes = {}
        self.path = os.path.join(os.getcwd(), "Meshes")
        # TODO: Determine this properly
        self.are_VBOs_supported = True
        
    def loadResources(self):
        """Call on engine init to load mesh files
        """
        for file in glob.glob( os.path.join(self.path, '*.xml') ):
            xmlDoc = xmlHandler.parse(file)
            xmlOuden = xmlDoc.getElementsByTagName("ouden")[0]
            for xmlMesh in xmlOuden.getElementsByTagName("mesh"):
                name, mesh = self._loadMesh(xmlMesh)
                self.meshes[name] = mesh
        
        # Add special-case\built-in meshes
        self.meshes["None"] = Mesh()
        self.meshes["none"] = self.meshes["None"]
        
        box_verts = [Mesh.Vertex(b2Vec2(-0.5, -0.5), b2Vec2(0, 0)), \
                     Mesh.Vertex(b2Vec2(0.5, -0.5), b2Vec2(1, 0)), \
                     Mesh.Vertex(b2Vec2(-0.5, 0.5), b2Vec2(0, 1)), \
                     Mesh.Vertex(b2Vec2(0.5, 0.5), b2Vec2(1, 1))]
        box_geom = { GL_TRIANGLE_STRIP: [0, 1, 2, 3] }
        self.meshes["Box"] = Mesh(box_verts, box_geom, \
                                  use_VBOs=self.are_VBOs_supported, \
                                  is_dynamic=False)
        self.meshes["box"] = self.meshes["Box"]

    def getUnnamedIndex(self):
        i = -1
        while True:
            i += 1
            yield i

    def _loadMesh(self, xmlMesh):
        # TODO: This code and the corresponding xml file format is a little messy and could do with a little more TLC
        if xmlMesh.hasAttribute("name"):
            name = xmlMesh.getAttribute("name")
        else:
            name = "unnamed_mesh_" + str(self.getUnnamedIndex().next())
        scale = float( xmlMesh.getAttribute("scale") )
        
        xmlVertexData = xmlMesh.getElementsByTagName("vertex_data")[0]
        vertices = []
        vertex_data = None
        format = xmlVertexData.getAttribute("format")
        if format == "xml":
            for xmlVertex in xmlVertexData.getElementsByTagName("vertex"):
                posString = xmlVertex.getAttribute("pos")
                pos = tuple( [float(x)*scale for x in (posString.split(","))] )
                texcoordsString = xmlVertex.getAttribute("texcoords")
                texcoords = tuple( [float(x) for x in (texcoordsString.split(","))] )
                # TODO: Load per-vertex coloring here
                vertices.append( Mesh.Vertex(b2Vec2(pos[0], pos[1]), b2Vec2(texcoords[0], texcoords[1])) )
        elif format == "binary":
            for xmlVertex in xmlVertexData.getElementsByTagName("vertex"):
                vertex_data = getXMLNodeText(xmlVertex)
        else:
            raise Exception("Unknown data format '" + format + "' in xml file.")

        xmlGeometry = xmlMesh.getElementsByTagName("geometry")[0]
        geometry = {}
        geometry_data = None
        for xmlIndexData in xmlGeometry.getElementsByTagName("index_data"):
                # TODO: Check if this should be done a better way
                mode = OpenGL.GL.__dict__[ xmlIndexData.getAttribute("mode") ]
                indicesString = getXMLNodeText(xmlIndexData)
                indices = tuple( [int(x) for x in (indicesString.split(","))] )
                geometry[mode] = indices
        # Note: I've decided not to allow binary-format index_data loading (as it was going to be a pain)
        """
        format = xmlGeometry.getAttribute("format")
        if format == "xml":
            for xmlIndexData in xmlGeometry.getElementsByTagName("index_data"):
                # TODO: Check if this should be done a better way
                mode = OpenGL.GL.__dict__[ xmlIndexData.getAttribute("mode") ]
                indicesString = getXMLNodeText(xmlIndexData)
                indices = tuple( [int(x) for x in (indicesString.split(","))] )
                geometry[mode] = indices
        
        elif format == "binary":
            geometry_data = {}
            for xmlIndexData in xmlGeometry.getElementsByTagName("index_data"):
                # TODO: Check if this should be done a better way
                mode = OpenGL.GL.__dict__[ xmlIndexData.getAttribute("mode") ]
                index_data = getXMLNodeText(xmlIndexData)
                geometry_data[mode] = index_data
        
        else:
            raise Exception("Unknown data format '" + format + "' in xml file.")
        """

        # TODO: Check how is_dynamic (and use_per_vertex_coloring) should be determined
        return (name, Mesh(vertices, geometry, vertex_data, geometry_data, self.are_VBOs_supported, use_per_vertex_coloring=False, is_dynamic=False))
        
    def getMesh(self, name):
        if name is None:
            return self.meshes["None"]
        elif name in self.meshes:
            mesh = self.meshes[name]
            if mesh.is_dynamic:
                # Return a new mesh instance with its own copy of the mesh data
                return Mesh(vertex_data=mesh.vertex_data, geometry_data=mesh.geometry_data, use_VBOs=self.are_VBOs_supported, is_dynamic=True)
            else:
                # Return the shared mesh instance that will never change
                return mesh
        else:
            raise Exception("Mesh '" + name + "' is not known (mesh path: '" + self.path + "')")

    
    def genBoxMesh(self, size):
        #N = 3 * 100
        #vertices = [Mesh.Vertex( b2Vec2(random.random()*4-2, random.random()*4-2), b2Vec2(random.random(), random.random()) ) for i in range(N)]
        
        vertices = [Mesh.Vertex(b2Vec2(-size[0], -size[1]), b2Vec2(0, 0)), \
                    Mesh.Vertex(b2Vec2(size[0], -size[1]), b2Vec2(1, 0)), \
                    Mesh.Vertex(b2Vec2(-size[0], size[1]), b2Vec2(0, 1)), \
                    Mesh.Vertex(b2Vec2(size[0], size[1]), b2Vec2(1, 1))]
        
        geometry = { GL_TRIANGLE_STRIP: [0, 1, 2, 3] }
        #geometry = { GL_TRIANGLES: [i for i in range(N)] }
        #print "Creating box mesh."
        return Mesh(vertices, geometry, use_VBOs=self.are_VBOs_supported, is_dynamic=False)
    """
    def generateLineMesh(self, width, *pointList):
        mesh = Mesh()
        # Algorithm to generate a mesh over a line of connected points with a
	# specified width (m_fSize) everywhere
	
	# First calculate the positions of the vertices
	
	numSegs = len(pointList) - 1;
	# NOTE: There will be a total of 2*numSegs+2 vertices
	#m_Vertices.resize( 2*iNumSegs + 2 );
	# TODO: This needs to be translated to Python
	
	# Calculate the first two vertices
	vSegDir = (m_pPoints[1]->vPos - m_pPoints[0]->vPos).getUnitVector();
	vSegNormalDir = vSegDir.getNormal();
	newVert.vPos = m_pPoints[0]->getPos() - m_fSize*(vSegDir+vSegNormalDir);
	m_Vertices[0] = newVert;
	newVert.vPos = m_pPoints[0]->getPos() - m_fSize*(vSegDir-vSegNormalDir);
	m_Vertices[1] = newVert;
	
	//Calculate the middle vertices
	for (int i=1;i<iNumSegs;++i) {
	    Vector vOldSegDir = vSegDir;
	    vSegDir = (m_pPoints[i+1]->getPos() -
		                            m_pPoints[i]->getPos()).getUnitVector();
	    Vector vSegDiff = vOldSegDir - vSegDir;
	    if ( 0 == vSegDiff )
		vSegDiff = vOldSegDir.getNormal();
		    Vector vMediator = vSegDiff * m_fSize /
		                                (vOldSegDir.getNormal() % vSegDiff);
	    
	    newVert.vPos = m_pPoints[i]->getPos() - vMediator;
		m_Vertices[2*i] = newVert;
		newVert.vPos = m_pPoints[i]->getPos() + vMediator;
		m_Vertices[2*i+1] = newVert;
	}
	
	//Calculate the last two vertices
	vSegDir = (m_pPoints[iNumSegs]->getPos() -
	                          m_pPoints[iNumSegs-1]->getPos()).UnitVector();
	vSegNormalDir = vSegDir.getNormal();
	newVert.vPos = m_pPoints[iNumSegs]->getPos() + m_fSize * (vSegDir - vSegNormalDir);
	m_Vertices[2*iNumSegs] = newVert;
	newVert.vPos = m_pPoints[iNumSegs]->getPos() + m_fSize * (vSegDir + vSegNormalDir);
	m_Vertices[2*iNumSegs+1] = newVert;
	
	//Now calculate the texture coordinates
	
	VertexList::iterator itVertex;
	for ( itVertex = m_Vertices.begin(), int i = 0;
	      itVertex != m_Vertices.end();
	      ++itVertex, ++i ) {
		int iEven = i % 2;
		//TODO: Check what effect the "- 2" has
		itVertex->vTexCoord = Vector( (double)(i - iEven) /
		                           (double)(iNumVerts - 2), (double)iEven );
	}
	
        return mesh
    """

# TODO: Work out where to put this
def getXMLNodeText(xmlNode):
    """Returns the text contained within an xml node
    """
    text = ""
    for node in xmlNode.childNodes:
       if node.nodeType == node.TEXT_NODE:
          text += node.data
    return text

class Model(object):
    """A renderable object that has a mesh with a texture and colour.
    """
    def __init__(self, renderer, layer, \
                 mesh, texture, \
                 use_vertex_influences=False, \
                 influences=[(b2XForm_identity, 1.0)], \
                 local_xform=b2XForm_identity, \
                 use_vertex_colors=False, color=(1.0, 1.0, 1.0, 1.0), \
                 scale=(1.0, 1.0)):

        # TODO: Add a .visible attribute for selective rendering

        #self.material = Material(renderer, None, None)
        #self.light = Light()

        self.renderer = renderer
        self.layer = layer
        self.use_vertex_influences=use_vertex_influences
        self.influences = influences
        self.local_xform = local_xform
        # Fancy conveniance parameter handling. Allows passing instances or ids.
        # TODO: Decide if this is a bad idea!
        if mesh.__class__ == Mesh: self.mesh = mesh
        else: self.mesh = self.renderer.meshLoader.getMesh(mesh)
        if texture.__class__ == Texture: self.texture = texture
        else: self.texture = self.renderer.textureLoader.getTexture(texture)
        self.use_vertex_colors = use_vertex_colors
        self.color = color
        self.scale = scale
        #if layer == None: self.layer = self.renderer.getDefaultLayer()
        #elif layer.__class__ == Model: self.layer = layer
        #else: self.layer = self.renderer.getLayer(layer)
        #self.skeleton = skeleton
    def draw(self):
        pass
        #self.layer.models_to_render.append(self)
    def _render(self):
        
        self.texture.bind()
        #self.light.bind()
        #self.material.bind()
        if not self.use_vertex_colors:
            glColor4f(self.color[0], self.color[1], self.color[2], self.color[3])
            
        glPushMatrix()
        
        # Calculate weighted sum of influence xforms
        if not self.use_vertex_influences:
            # TODO: See if there is a faster way to perform these weighted
            #       transform sums. Could sum in matrix space rather
            #       than pos\angle space?
            #influence_xform = b2XForm_identity
            pos = b2Vec2(0, 0)
            angle = 0
            for influence in self.influences:
                #                     xform     *   weight
                #influence_xform.position += influence[0].position * influence[1]
                pos += influence[0].position * influence[1]
                angle += influence[0].R.GetAngle() * influence[1]
            #influence_xform.R = b2Mat22(angle)
            influence_xform = b2XForm(pos, b2Mat22(angle))
            glMultMatrixf(influence_xform.GetOpenGLForm())
        
        #self.local_xform.GetOpenGLForm()
        glMultMatrixf(self.local_xform.GetOpenGLForm())
        # Apply local scale transform
        glScalef(self.scale[0], self.scale[1], 1.0)
        self.mesh.render()
        glPopMatrix()
        
# TODO: This is a custom addition to Box2D functionality
def GetOpenGLForm(self):
    """
    Generate a 4x4 transformation matrix in a form that OpenGL can accept
    (it appears here as the transpose of the normal transformation matrix)
    """
    return [self.R.col1.x,   self.R.col1.y,   0, 0, \
            self.R.col2.x,   self.R.col2.y,   0, 0, \
            0,               0,               1, 0, \
            self.position.x, self.position.y, 0, 1]
# Add the method to the b2XForm class
setattr(b2XForm, "GetOpenGLForm", GetOpenGLForm)
